fix(pds-core): identify generated-handle accounts by email - #244
Conversation
🦋 Changeset detectedLatest commit: 671c4e7 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Review limit reached
Next review available in: 12 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR adds email-first identity presentation across consent, chooser, and account-management views. It resolves handle mode from request and client metadata, exposes generated handles through accessible descriptions and tooltips, and adds comprehensive unit, integration, and end-to-end coverage. ChangesEmail-first identity presentation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant OAuthClient
participant ConsentPreview
participant ChooserEnrichment
participant AccountChooser
OAuthClient->>ConsentPreview: OAuth request with handle mode context
ConsentPreview->>ChooserEnrichment: resolved mode and session hydration data
ChooserEnrichment->>AccountChooser: email-first identity markup
AccountChooser->>ChooserEnrichment: focus identity information button
ChooserEnrichment-->>AccountChooser: accessible handle tooltip
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🚅 Deployed to the ePDS-pr-244 environment in ePDS
|
There was a problem hiding this comment.
Pull request overview
This PR completes the “email-first identity for generated handles” work in pds-core by extending the chooser enrichment script to correctly identify accounts by email (when handle mode is random) across OAuth chooser/consent and account-management surfaces, while keeping the public handle available via accessible UI affordances.
Changes:
- Extends the injected enrichment script to cover consent identity text,
/accountlist rows, and/account/:didaccount selector UI, and fixes the upstream__sessions/__deviceSessionscapture clobbering. - Updates preview routes (
/preview/chooser,/preview/consent) to exercise the same handle-mode resolution path and inject the enrichment script ahead of hydration. - Expands unit/e2e coverage to assert the new accessible-description behavior (instead of
title=) and adds consent scenarios.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/pds-core/src/lib/preview-shared.ts | Adds shared resolveQueryHandleMode() so both preview routes resolve handle mode consistently. |
| packages/pds-core/src/lib/preview-consent.ts | Injects handle-mode meta + enrichment script into consent preview, and wires handle mode into the fixture. |
| packages/pds-core/src/lib/preview-chooser.ts | Removes duplicated handle-mode resolution and uses the shared preview resolver. |
| packages/pds-core/src/index.ts | Passes PAR client-id resolver + logger into chooser enrichment middleware wiring. |
| packages/pds-core/src/chooser-enrichment.ts | Major enrichment-script expansion (consent/account surfaces, accessible descriptions/tooltips) and request-context handle-mode resolution via PAR request_uri. |
| packages/pds-core/src/tests/preview-consent.test.ts | Verifies consent preview injects handle-mode meta + enrichment script before hydration and supports ?epds_handle_mode=random. |
| packages/pds-core/src/tests/preview-chooser.test.ts | Verifies enrichment script precedes hydration and fixture identities are present. |
| packages/pds-core/src/tests/chooser-enrichment.test.ts | Large expansion of deterministic DOM tests for consent/account enrichment and accessibility behavior. |
| features/session-reuse-bugs.feature | Updates feature wording to match accessible-description behavior. |
| features/consent-screen.feature | Adds consent scenarios covering tooltip identity behavior for default vs random handle modes. |
| e2e/step-definitions/session-reuse-bugs.steps.ts | Updates e2e assertions from title= tooltip to aria-describedby hidden-handle descriptions. |
| e2e/step-definitions/consent.steps.ts | Adds e2e steps validating consent email-first identity and tooltip-exposed handle/email. |
| .changeset/email-first-account-presentation.md | Adds end-user-facing release notes for email-first identity + accessible info icon behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
daa7cea to
84de1ac
Compare
Same leak as the CSS middleware: request_uri is a short-lived bearer reference to the PAR entry, so logging its value makes it replayable. Raised by Copilot on #244. The existing rejection test covered the client_id path, where there is no request_uri to leak, so it asserted the field was undefined without exercising the branch that carries a value. Fix its assertion and add a test on the request_uri path that pins the value out of the log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e7a7a67 to
6afaf71
Compare
|
Fixed in - { err, requestUri: query.request_uri, queryMode },
+ {
+ err,
+ hasRequestUri: typeof query.request_uri === 'string',
+ queryMode,
+ },The existing rejection test here looked like coverage but wasn't: it exercised the |
84de1ac to
14dd1b4
Compare
Same leak as the CSS middleware: request_uri is a short-lived bearer reference to the PAR entry, so logging its value makes it replayable. Raised by Copilot on #244. The existing rejection test covered the client_id path, where there is no request_uri to leak, so it asserted the field was undefined without exercising the branch that carries a value. Fix its assertion and add a test on the request_uri path that pins the value out of the log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6afaf71 to
87b55c7
Compare
bf37444 to
534647d
Compare
Same leak as the CSS middleware: request_uri is a short-lived bearer reference to the PAR entry, so logging its value makes it replayable. Raised by Copilot on #244. The existing rejection test covered the client_id path, where there is no request_uri to leak, so it asserted the field was undefined without exercising the branch that carries a value. Fix its assertion and add a test on the request_uri path that pins the value out of the log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
87b55c7 to
63a3a66
Compare
When a handle is server-generated the user never chose it and cannot recognise it, so leading with it on the chooser and consent screens tells them nothing about which account they are approving. Show the email as the primary identifier in that case and keep the handle reachable through an information icon, since the handle is still the public name AT Protocol apps display. Scope the DOM rewriting to identity elements this code can actually identify: an exact match against a known account's handle, @handle or sub, on a b/strong element, in one of the recognised consent phrasings. The previous heuristic matched handle-shaped text anywhere on the page, which put it in reach of legal copy, connected-app rows and device rows that happened to contain something handle-like. Replaces the title= tooltip, which screen readers and touch devices handle poorly, with a real role="tooltip" wired through aria-describedby. Escape dismisses it, as WCAG 1.4.13 requires for content shown on hover or focus. Also fixes the two globals sharing a single capture variable: upstream sets __sessions or __deviceSessions depending on the route, and whichever wrote last clobbered the other. Extends coverage to the consent pages, /account, /account/:did and the preview routes, and updates the e2e assertions to check the accessible description rather than the old title attribute. Split out of #148. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same leak as the CSS middleware: request_uri is a short-lived bearer reference to the PAR entry, so logging its value makes it replayable. Raised by Copilot on #244. The existing rejection test covered the client_id path, where there is no request_uri to leak, so it asserted the field was undefined without exercising the branch that carries a value. Fix its assertion and add a test on the request_uri path that pins the value out of the log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
63a3a66 to
9277fc1
Compare
Coverage Report for CI Build 31538892005Coverage increased (+0.02%) to 60.552%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
packages/pds-core/src/chooser-enrichment.ts (2)
482-529: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winHoist the per-tick helper declarations out of
enrich().
accountListAnchor,emptyAccountTitle, andenrichAccountListRoware declared insideenrich().enrich()runs from theMutationObservercallback on Line 673, which observesdocument.documentElementwithsubtree: true.enrich()itself mutates the DOM: it appends spans, setsstyleproperties, and sets attributes. Each of those mutations schedules another tick.The
datasetguards make repeat ticks cheap per node, but each tick still allocates three closures and performs a fullTreeWalkerpass over#root, plus thequerySelectorAllcalls inenrichAccountSelectorandhideSignup. This PR increases the per-tick cost with the added consent walker and selector walker.Move the three helpers to the same scope as the other top-level script functions. Consider also coalescing observer ticks with
requestAnimationFrameso a burst of self-inflicted mutations produces one pass.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/pds-core/src/chooser-enrichment.ts` around lines 482 - 529, Move accountListAnchor, emptyAccountTitle, and enrichAccountListRow out of enrich() into the same top-level scope as the other script functions, preserving their existing behavior and dependencies. Keep enrich() focused on orchestration so observer-triggered ticks do not recreate these closures; optionally coalesce MutationObserver-triggered enrich passes with requestAnimationFrame if supported by the existing flow.
294-300: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMake consent identity enrichment locale-independent.
@atproto/oauth-provider-ui@0.4.3falls back tonavigator.languageswhenuiLocalesis absent. French and Japanese consent copy therefore bypasseshasApprovedConsentIdentityContext. Match a structurally identified account element while retaining the exact known handle,@handle, or DID check.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/pds-core/src/chooser-enrichment.ts` around lines 294 - 300, Update hasApprovedConsentIdentityContext to identify the consent account element structurally rather than matching only English phrases in context.before, so localized French and Japanese copy is supported. Preserve the existing exact checks for a known handle, `@handle`, or DID, and keep the context.after === 'account' requirement.packages/pds-core/src/__tests__/preview-consent.test.ts (1)
83-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winIsolate
EPDS_DEFAULT_HANDLE_MODEin this test.
resolveHandleModereads this variable at render time. Stub it to an empty value before rendering and callvi.unstubAllEnvs()in the existingafterEach;vi.restoreAllMocks()does not restore environment stubs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/pds-core/src/__tests__/preview-consent.test.ts` around lines 83 - 103, Update the test around createPreviewConsentHandler to stub EPDS_DEFAULT_HANDLE_MODE to an empty value before rendering, ensuring resolveHandleMode reads the isolated environment. Extend the existing afterEach cleanup to call vi.unstubAllEnvs() alongside vi.restoreAllMocks().</codeેનpackages/pds-core/src/__tests__/chooser-enrichment.test.ts (2)
1939-1955: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winMake the redaction guard see error messages.
JSON.stringifyserializes anErrorinstance to{}, becausemessageandstackare non-enumerable. Theerrfield in the logged payload is therefore invisible to thenot.toContain(requestUri)guard. The test cannot catch a regression where the request URI reaches the log through an error message.Normalize errors before stringifying so the guard covers the whole payload.
🔒️ Proposed hardening
expect(debug).toHaveBeenCalledWith( expect.objectContaining({ hasRequestUri: true }), 'chooser-enrichment: failed to resolve handle mode from OAuth request context', ) - expect(JSON.stringify(debug.mock.calls)).not.toContain(requestUri) + const serialized = JSON.stringify(debug.mock.calls, (_key, value) => + value instanceof Error ? `${value.name}: ${value.message}` : value, + ) + expect(serialized).not.toContain(requestUri)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/pds-core/src/__tests__/chooser-enrichment.test.ts` around lines 1939 - 1955, Update the redaction assertion in the “logs request_uri presence but never its value” test to normalize Error values, including their message and stack, before stringifying debug.mock.calls. Keep the existing request URI absence check, but ensure it inspects the complete logged payload so a URI embedded in an error message is detected.
1842-1855: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert that
resolveClientMetadatais not called after client-id resolution rejects.The test supplies
resolveClientMetadata: vi.fn()but never asserts on it.vi.fn()returnsundefined, so a regression that callsresolveClientMetadata(undefined)after the rejection still yields the fallback meta tag and the test still passes. Assert the skip explicitly.♻️ Proposed assertion
it('degrades silently when request_uri client-id resolution rejects', async () => { + const resolveClientMetadata = vi.fn() + const written = await captureWrittenHtml( { - resolveClientMetadata: vi.fn(), + resolveClientMetadata, resolveClientIdFromRequestUri: () => Promise.reject(new Error('request expired')), }, { request_uri: 'urn:ietf:params:oauth:request_uri:req-123' }, ) + expect(resolveClientMetadata).not.toHaveBeenCalled() expect(written).toContain( '<meta name="epds-handle-mode" content="picker-with-random">', ) })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/pds-core/src/__tests__/chooser-enrichment.test.ts` around lines 1842 - 1855, Update the test case around captureWrittenHtml to assert that the supplied resolveClientMetadata mock was not called when resolveClientIdFromRequestUri rejects. Keep the existing fallback meta-tag assertion and use the existing resolveClientMetadata mock for the explicit call-count check.e2e/step-definitions/consent.steps.ts (1)
69-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake
openIdentityTooltipidempotent.The tooltip control toggles on click. Line 82 asserts
aria-expandedis'false'before line 90 clicks it. Two Then steps call this helper: "the consent identity tooltip exposes the public AT Protocol handle" and "the consent identity tooltip exposes the account email". No scenario chains both steps today, so the helper works. If a future scenario chains them, the second call finds the tooltip already open and the line 82 precondition fails with a misleading message.Open the tooltip only when it is closed.
♻️ Proposed change
await expect(tooltipControl).toHaveAttribute('type', 'button') - await expect(tooltipControl).toHaveAttribute('aria-expanded', 'false') const describedBy = await tooltipControl.getAttribute('aria-describedby') expect(describedBy?.trim()).toBeTruthy() if (!describedBy?.trim()) { throw new Error('Expected aria-describedby to reference a tooltip') } const [tooltipId] = describedBy.trim().split(/\s+/) - await tooltipControl.click() + // Idempotent: a scenario may chain several tooltip assertions, and the + // control toggles rather than latches open. + if ((await tooltipControl.getAttribute('aria-expanded')) !== 'true') { + await tooltipControl.click() + } await expect(tooltipControl).toHaveAttribute('aria-expanded', 'true')🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@e2e/step-definitions/consent.steps.ts` around lines 69 - 97, Update openIdentityTooltip so it reads aria-expanded and clicks the tooltip control only when its current value is not already 'true'. Preserve the existing closed-state validation for the initial call, while allowing subsequent calls to reuse the already-open tooltip and continue validating and returning it.e2e/step-definitions/session-reuse-bugs.steps.ts (1)
440-478: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert that the hidden-handle description reaches the accessibility tree.
The step proves the description element exists, carries
.epds-hidden-handle-description, and holds the expected text. It does not prove that assistive technology can announce it. If the enrichment styled the description withdisplay: noneorvisibility: hidden, screen readers would announce nothing and this step would still pass. That defeats the accessibility guarantee this PR introduces.Capture the computed style of the resolved description and assert it is not removed from the accessibility tree.
♻️ Proposed additions
type HiddenHandleDescriptionRow = { describedBy: string | null descriptions: { id: string isHiddenHandleDescription: boolean + isAccessible: boolean text: string }[]const descriptions = descriptionIds.map((id) => { const describedElement = document.getElementById(id) + const style = describedElement + ? globalThis.getComputedStyle(describedElement) + : null return { id, isHiddenHandleDescription: describedElement?.classList.contains( 'epds-hidden-handle-description', ) ?? false, + // display:none / visibility:hidden remove the node from the + // accessibility tree, so aria-describedby would announce nothing. + isAccessible: + style !== null && + style.display !== 'none' && + style.visibility !== 'hidden', text: describedElement?.textContent?.trim() ?? '', } })Then assert it in the loop:
expect( description, `Row ${row.rowIndex}: expected aria-describedby to reference an .epds-hidden-handle-description element`, ).toBeDefined() + + expect( + description?.isAccessible, + `Row ${row.rowIndex}: hidden-handle description must stay in the accessibility tree (not display:none / visibility:hidden)`, + ).toBe(true)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@e2e/step-definitions/session-reuse-bugs.steps.ts` around lines 440 - 478, Update the evaluateAll callback and its HiddenHandleDescriptionRow result to capture each resolved description element’s computed display and visibility values. In the assertion loop consuming these rows, require the hidden-handle description to have display other than none and visibility other than hidden, while preserving the existing class and text checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/pds-core/src/chooser-enrichment.ts`:
- Around line 407-409: Update the publicIdentifier construction in both
accessible-name paths near the shown code and the corresponding later site so a
matched DID fallback does not pass through formatPublicHandle or produce an `@did`
value. Only include the formatted handle when preferred_username or an
`@-prefixed` handle is available; otherwise omit the handle segment from the
aria-label while preserving existing username behavior.
---
Nitpick comments:
In `@e2e/step-definitions/consent.steps.ts`:
- Around line 69-97: Update openIdentityTooltip so it reads aria-expanded and
clicks the tooltip control only when its current value is not already 'true'.
Preserve the existing closed-state validation for the initial call, while
allowing subsequent calls to reuse the already-open tooltip and continue
validating and returning it.
In `@e2e/step-definitions/session-reuse-bugs.steps.ts`:
- Around line 440-478: Update the evaluateAll callback and its
HiddenHandleDescriptionRow result to capture each resolved description element’s
computed display and visibility values. In the assertion loop consuming these
rows, require the hidden-handle description to have display other than none and
visibility other than hidden, while preserving the existing class and text
checks.
In `@packages/pds-core/src/__tests__/chooser-enrichment.test.ts`:
- Around line 1939-1955: Update the redaction assertion in the “logs request_uri
presence but never its value” test to normalize Error values, including their
message and stack, before stringifying debug.mock.calls. Keep the existing
request URI absence check, but ensure it inspects the complete logged payload so
a URI embedded in an error message is detected.
- Around line 1842-1855: Update the test case around captureWrittenHtml to
assert that the supplied resolveClientMetadata mock was not called when
resolveClientIdFromRequestUri rejects. Keep the existing fallback meta-tag
assertion and use the existing resolveClientMetadata mock for the explicit
call-count check.
In `@packages/pds-core/src/__tests__/preview-consent.test.ts`:
- Around line 83-103: Update the test around createPreviewConsentHandler to stub
EPDS_DEFAULT_HANDLE_MODE to an empty value before rendering, ensuring
resolveHandleMode reads the isolated environment. Extend the existing afterEach
cleanup to call vi.unstubAllEnvs() alongside vi.restoreAllMocks().</codeેન
In `@packages/pds-core/src/chooser-enrichment.ts`:
- Around line 482-529: Move accountListAnchor, emptyAccountTitle, and
enrichAccountListRow out of enrich() into the same top-level scope as the other
script functions, preserving their existing behavior and dependencies. Keep
enrich() focused on orchestration so observer-triggered ticks do not recreate
these closures; optionally coalesce MutationObserver-triggered enrich passes
with requestAnimationFrame if supported by the existing flow.
- Around line 294-300: Update hasApprovedConsentIdentityContext to identify the
consent account element structurally rather than matching only English phrases
in context.before, so localized French and Japanese copy is supported. Preserve
the existing exact checks for a known handle, `@handle`, or DID, and keep the
context.after === 'account' requirement.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a2971628-d4d2-4851-b956-287089c78113
📒 Files selected for processing (13)
.changeset/email-first-account-presentation.mde2e/step-definitions/consent.steps.tse2e/step-definitions/session-reuse-bugs.steps.tsfeatures/consent-screen.featurefeatures/session-reuse-bugs.featurepackages/pds-core/src/__tests__/chooser-enrichment.test.tspackages/pds-core/src/__tests__/preview-chooser.test.tspackages/pds-core/src/__tests__/preview-consent.test.tspackages/pds-core/src/chooser-enrichment.tspackages/pds-core/src/index.tspackages/pds-core/src/lib/preview-chooser.tspackages/pds-core/src/lib/preview-consent.tspackages/pds-core/src/lib/preview-shared.ts
matchAccountIdentifier also matches on account.sub, so the matched text can be a DID when an account has no preferred_username. Both aria-label sites and the random-mode tooltip then ran that text through formatPublicHandle(), which prepends '@' to anything not already starting with one — yielding "@did:plc:..." and asserting a false identifier type. This lands in accessible names and tooltip copy, so the wrong claim is read out to exactly the users this enrichment exists to help. Add publicIdentifierFor() to skip the '@' decoration for DIDs, and publicIdentityTooltip() to describe a DID as an identifier rather than a handle. Both new tests fail against the previous behaviour. Reported by CodeRabbit on #244; the tooltip site was not in the original report but shares the same fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JSON.stringify renders an Error as {} because message and stack are
non-enumerable, so the logged `err` field was invisible to the
not.toContain(requestUri) assertion. The test therefore could not fail —
including for the most plausible leak path, a request_uri embedded in the
rejection message of the very lookup this code logs about.
Normalize Errors during serialization so the guard covers the whole
payload. Verified by planting the URI in the rejected Error's message:
the assertion now fails and reports the leak, where before it passed.
Reported by CodeRabbit on #244.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
(reply generated by Claude Opus 5 via Claude Code) Working through the 7 nitpicks in the CodeRabbit review summary (these were in the review body, not posted as inline threads). Fixed
Deferred, with reasoning
Both inline threads are addressed and replied to individually: 9277fc1 for the |
|



Split 4 of 4 from #148 — the user-visible change the other three exist to support. #241, #242 and #243 have all merged, so this now targets
maindirectly and contains only its own two commits.Closes #143.
What it does
When a handle is server-generated, the user never chose it and cannot recognise it, so leading with it on the chooser and consent screens tells them nothing about which account they are approving. This shows the email as the primary identifier in that case, and keeps the handle reachable through an information icon — the handle is still the public name AT Protocol apps display, so it can't just be dropped.
UI Changes
when handle mode is
pickerorpicker-with-randomwhen handle mode is
random— email leads, and the handle stays available through the information iconTightened DOM matching
The previous heuristic matched handle-shaped text anywhere on the page, which put it in reach of legal copy, connected-app rows, device rows and any prose that happened to contain something handle-like.
Rewriting is now scoped to elements this code can actually identify: an exact match against a known account's
preferred_username,@handleorsub, on ab/strongelement, inside one of the recognised consent phrasings.This remains coupled to upstream's DOM copy —
hasApprovedConsentIdentityContexthard-matches strings like"wants to access your"and"account". The fixture tests will not catch an upstream copy change, so this is worth re-verifying against the real bundle after any upstream upgrade. It collides with #233 for exactly this reason.Accessibility
Replaces the
title=tooltip — which screen readers and touch devices handle poorly — with a realrole="tooltip"wired througharia-describedby. Hover, focus and tap all open it; tap/click pins it; Escape dismisses it, as WCAG 1.4.13 requires for content shown on hover or focus.Also fixed
The two upstream globals shared a single capture variable. Upstream sets
__sessionsor__deviceSessionsdepending on the route, and whichever wrote last clobbered the other.Coverage
Extends enrichment to the consent pages,
/account,/account/:didand the preview routes. e2e assertions now check the accessible description rather than the oldtitleattribute.Verification
typecheck,lint,formatclean; 76 test files / 1181 tests pass.Series
epds_handle_modethrough the callback hop ✅ merged🤖 Generated with Claude Code
Summary by CodeRabbit